﻿/* 
 * Flash Graph Visualization Toolkit
 * Goal: To provide a simple interactive toolkit for visualizing graph structure and end countless
 * fruitless goolge:"actionscript graph -chart" queries.
 *
 * Acknowledgements: 
 * This toolkit owes a debt of gratitude to many people. Mostly the early paper on this topic:
 * "Graph Drawing by Force-directed Placement" by Fruchterman & Reingold
 * [http://www.cs.ubc.ca/rr/proceedings/spe91-95/spe/vol21/issue11/spe060tf.pdf]
 *
 * And also to Marcos Weskamp's beautiful FlickrGraph, which we've spent countless hour playing
 * with; he just got it -right-. This is pretty close, but not quite.
 * [http://www.marumushi.com/apps/flickrgraph/]
 * 
 * And of course to the Macromedia/Adobe team that brings us flash. Rot in hell Ajax.
 * 
 * Final Note: Don't like this? Rip out the render, attractForce, repelForce and do your own thing.
 */

import Delegate;
import flash.geom.Point;

class GraphVis extends MovieClip {	
	
	public var idealSegmentLength:Number;
	private var DeltaLimit:Number;
	public var BOUNCE:Number;
	public var SPRINGK:Number;
	public var REPELK:Number;
	public var RESISTANCE:Number;
	public var MASS:Number;
	public var GRAVITY:Number;
	private var STOPVEL:Number;
	private var STOPACC:Number;
	private var STOPRENDER:Number;

	public var nodeList:Array;
	public var edgeList:Array;
	
	private var isRendering:Boolean;
	private var GraphFrame:MovieClip;
	private var EdgeFrame:MovieClip;
	
	private var EdgeCanvas:MovieClip;
	public var edgeColor;
	
	function GraphVis() {
		EdgeCanvas = GraphFrame.EdgeFrame.createEmptyMovieClip("EdgeCanvas", 0);
		EdgeCanvas._x = 0; EdgeCanvas._y = 0;
		
		// Be careful screwing with these, you may get some crazyness
		idealSegmentLength = 35; 
		DeltaLimit = .02; // was .02
		BOUNCE = .9; // Not used
		SPRINGK = 10; REPELK = 10; RESISTANCE = 10; 
		MASS = 2; GRAVITY = 0; STOPVEL = 3; STOPACC = 3; STOPRENDER = .01; // was .1 .1 .01
	} 
	
	public function setUp(nList, eList) {
		nodeList = nList;
		edgeList = eList;
		var initCounter = 0;
		
		// Do an initial adjustment
		while(initCounter < 0)
		{
			renderGraph();
			
			initCounter++;
		}
		setRendering(true);
	}
	
	public function setRendering(bool:Boolean):Void {
		isRendering = bool;
		if (bool){
			startRendering();
		} else {
			stopRendering();
		}
	}
	
	public function getRendering():Boolean {
		return isRendering;
	}
	
	public function startRendering():Void {
		GraphFrame.onEnterFrame = Delegate.create(this, renderGraph);
	}
	
	public function stopRendering():Void {
		isRendering = false;
		delete GraphFrame.onEnterFrame;
	}
	
	
	private function renderGraph():Void {
		var currentXPos = 0;
		var currentYPos = 0;
	
		// for all the nodes in the nodelist
		for (var i=0; i < nodeList.length; i++) { 

			var currentNode = nodeList[i]; // get the actual node

			if (currentNode != _root.selectedNode && currentNode._visible != false && currentNode.cluster == _root.selectedNode.cluster)  //do nothing for the selected node
			{
				var springVector = new Point(0, 0); // use point class, mean vector
			
				// Attract all connected nodes
				for (var j=0; j < currentNode.connectedList.length; j++) {
					if(currentNode.connectedList[j]._visible != false && currentNode.connectedList[j].cluster == currentNode.cluster) {
						var currentConnectedNode = currentNode.connectedList[j];
						springVector = attractForce(currentConnectedNode, currentNode, springVector);
					}
				}
			
				// Repel all the other nodes
				for (var j=0; j < currentNode.notConnectedList.length; j++) {
					if(currentNode.notConnectedList[j]._visible != false && currentNode.notConnectedList[j].cluster == currentNode.cluster) {
						var currentNotConnectedNode = currentNode.notConnectedList[j];
						springVector = repelForce(currentNotConnectedNode, currentNode, springVector);
					}
				}
				

				// calculate resistance
				var resistanceVector = new Point(-currentNode.dx * RESISTANCE, -currentNode.dy * RESISTANCE);
			
				// adjust for mass and gravity
				var massGravityVector = new Point(((springVector.x + resistanceVector.x)) / MASS, (((springVector.y + resistanceVector.y)) / MASS + GRAVITY));
			
				// Curb our enthusiasm, and set the delta x and delta y values.
				currentNode.dx +=  DeltaLimit * massGravityVector.x;
				currentNode.dy +=  DeltaLimit * massGravityVector.y;
			
				// Slow down
				if (((Math.abs(currentNode.dx) < STOPVEL && Math.abs(currentNode.dy) < STOPVEL) && Math.abs(massGravityVector.x) < STOPACC) && (Math.abs(massGravityVector.y) < STOPACC)) {
					currentNode.dx = 0;
					currentNode.dy = 0;
				}

						
				if(_root.LineLock != "y") {currentNode._x = Math.min(Math.max(currentNode.NodeImage._width/2, currentNode._x +=  currentNode.dx), _root.GraphWidth-currentNode.NodeImage._width/2); }// adjust the x position by the change in x
				if(_root.LineLock != "x") {currentNode._y = Math.min(Math.max(currentNode.NodeImage._height/2, currentNode._y +=  currentNode.dy), _root.GraphHeight-currentNode.NodeImage._height/2);} // adjust the y position by the change in y
				// edge bounce
				if(currentNode._x <= currentNode.NodeImage._width/2 || currentNode._x >= _root.GraphWidth-currentNode.NodeImage._width/2) {
					currentNode.dx = currentNode.dx * -0.33;
				}
				if(currentNode._y <= currentNode.NodeImage._height/2 || currentNode._y >= _root.GraphHeight-currentNode.NodeImage._height/2) {
					currentNode.dy = currentNode.dy * -0.33;
				}
								
				currentXPos = (currentXPos + currentNode.dx);
				currentYPos = (currentYPos + currentNode.dy);
				
				
			} // end if not selected node
		} // end nodeList iteration
		
		if (isRendering){
			moveNodes();
			drawEdges();
			rotateNodes();
			if(_root.viewMode == "explore") {toggleConnections();}
		}
		
		// If we're done...
		if ((Math.abs(currentXPos) + Math.abs(currentYPos)) < STOPRENDER && (_root.isDragging == false)) {		
			trace("STop Render");
			stopRendering();
		}
	}
	

	function toggleOff() {
		if(_root.viewMode == "explore") {
		var currentNode = _root.selectedNode;
		
		
		for (var j=0; j < currentNode.notConnectedList.length; j++) {
			var currentNotConnectedNode = currentNode.notConnectedList[j];
			currentNotConnectedNode.MoreIndicator._visible = false;		
			
			if(currentNotConnectedNode != _root.selectedNode){
			currentNotConnectedNode._visible = false;
			currentNotConnectedNode._x = currentNode._x + 1;
			currentNotConnectedNode._y = currentNode._y + 1;
			}
		}
		}
	}
	function toggleConnections() {

				var currentNode = _root.selectedNode;
						
				for (var j=0; j < currentNode.connectedList.length; j++) {
					var currentConnectedNode = currentNode.connectedList[j];
					if(currentConnectedNode._visible != true) {currentConnectedNode._visible = true;}
					
					if(currentConnectedNode.connectedList.length > 1 ) { currentConnectedNode.MoreIndicator._visible = true;}									
				}		
				
						
			
	}

	function rotateNodes() {
		for (var i=0; i < nodeList.length; i++) {
		var aNode = nodeList[i];
			if(aNode._visible != false && aNode.cluster == _root.selectedNode.cluster) {
				var degrees;
				if(aNode == _root.selectedNode) {
					degrees = 0;
				}
				else {
				var radians = Math.atan2(aNode._y-_root.selectedNode._y, aNode._x-_root.selectedNode._x);
		         degrees = Math.round((radians*180)/Math.PI);
				}

				var dist:Number = Math.abs(degrees - aNode._rotation);
			   aNode._rotation %= 360;

			   if(dist > 180)     {
			      if(degrees > aNode._rotation) {
			         degrees -= 360;
			      } else {
			         degrees += 360;
			      }
			   }
				if(aNode._rotation < degrees) {
				aNode._rotation = Math.min(aNode._rotation+(dist*.25),degrees);
				}	
				if(aNode._rotation > degrees) {
				aNode._rotation = Math.max(aNode._rotation-(dist*.25),degrees);
				}	
			}
		}
	}
	function moveNodes() {
		for (var i=0; i < nodeList.length; i++) {
			var aNode = nodeList[i];
			if(aNode._visible != false) {
				aNode._x = Math.floor(aNode._x);
				aNode._y = Math.floor(aNode._y);
				if(aNode._x < 0) { aNode._x = aNode.NodeImage._width/2; }
				if(aNode._y < 0) { aNode._y = aNode.NodeImage._height/2; }
				if(aNode._x > _root.GraphWidth-aNode.NodeImage._width/2) { aNode._x = _root.GraphWidth-aNode.NodeImage._width/2; }
				if(aNode._y > _root.GraphHeight-aNode.NodeImage._height/2) { aNode._y = _root.GraphHeight-aNode.NodeImage._height/2; }
		}



		}
	}	
	
	private function drawEdges() {
		EdgeCanvas.clear();
		EdgeCanvas.lineStyle(2, edgeColor, 100);
		for (var i=0; i < edgeList.length; i++) {
			var edge = edgeList[i];
			if(_root.viewMode != "explore" || edge.fromNode == _root.selectedNode || edge.toNode == _root.selectedNode) {
			drawEdge(edge); } else { edge.mc._visible = false; }
		}
	}
	private function drawEdge(edge:Edge) {
		var fromNode = edge.fromNode;
		var toNode = edge.toNode;
		var xLength = (toNode._x - fromNode._x);
		var yLength = (toNode._y - fromNode._y);
		
		if (fromNode._x != undefined && toNode._x != undefined){
			EdgeCanvas.moveTo(fromNode._x, fromNode._y);
			EdgeCanvas.lineTo(toNode._x, toNode._y);
			
			if(_root.GraphDirection == "directed") {
				var radians = Math.atan2(fromNode._y-toNode._y, fromNode._x-toNode._x);
				var degrees = radians * (180/Math.PI);
			
				var degreesUp = degrees+5;
				var degreesDown = degrees-5;
				var radiansUp = degreesUp* (Math.PI/180);
				var radiansDown = degreesDown* (Math.PI/180);
				var targetWidth = (toNode.NodeBG._width/2) ;
				var targetHeight = (toNode.NodeBG._height/2);
				var adjust = 10;

				var edgex1 = (toNode._x + ((targetWidth) * Math.cos(radians)));
				var edgey1 = (toNode._y + ((targetHeight) * Math.sin(radians)));
				var edgex2 = (toNode._x + ((targetWidth+adjust) * Math.cos(radiansUp)));
				var edgey2 = (toNode._y + ((targetHeight+adjust) * Math.sin(radiansUp)));
				var edgex3 = (toNode._x + ((targetWidth+adjust) * Math.cos(radiansDown)));
				var edgey3 = (toNode._y + ((targetHeight+adjust) * Math.sin(radiansDown)));
			
				EdgeCanvas.lineStyle(2, edgeColor, 100);
				EdgeCanvas.moveTo(edgex1, edgey1);	
				EdgeCanvas.beginFill(edgeColor);					
				EdgeCanvas.lineTo(edgex2, edgey2);
				EdgeCanvas.lineTo(edgex3, edgey3);
				EdgeCanvas.lineTo(edgex1, edgey1);
			
				EdgeCanvas.endFill();			
				EdgeCanvas.lineStyle(2, edgeColor, 100);
			}
	
		}
		var edgeClip = edge.mc;
		edge.mc._visible = true;
		// this is particular to debug
		var hypotenuse:Number = Math.sqrt((xLength) * (xLength) + (yLength) * (yLength));
		
		edge.mc.debug.text = "(" + Math.round(hypotenuse) + "px)";
		edgeClip._x = xLength / 2 + fromNode._x;
		edgeClip._y = yLength / 2 + fromNode._y;
	}
	
	/**
	* Function which calculates the attractive force of nodes that are connected.
	*
	* @param fromNode the node that is remaining stationary 
	* @param toNode the connected that is to be attracted
	* @param spring the vector of spring forces 
	* @return the spring vector adjusted for attraction
	*/
	private function attractForce(fromNode:Node, toNode:Node, spring:Point):Point {

		var diffX:Number = (fromNode._x - toNode._x);
		var diffY:Number = (fromNode._y - toNode._y);
		var hypotenuse:Number = Math.sqrt(diffX * diffX + diffY * diffY);
		if (hypotenuse > (idealSegmentLength + fromNode._width/4)){
			var adjusted:Number = SPRINGK * (hypotenuse - (idealSegmentLength + fromNode._width/4));
			spring.x +=  diffX / hypotenuse * adjusted;
			spring.y +=  diffY / hypotenuse * adjusted;
		}
		return spring;
	}
	
   /**
	* Function which calculated the repulsive force of nodes that are not connected.
	*
	* @param fromNode the node that is remaining stationary 
	* @param toNode the unconnected that is to be repulsed
	* @param spring the vector of spring forces 
	* @return the spring vector adjusted for repulsion
	*/
	private function repelForce(fromNode:Node, toNode:Node, spring:Point):Point {
		var diffX:Number = (fromNode._x - toNode._x);
		var diffY:Number = (fromNode._y - toNode._y);
		var hypotenuse:Number = Math.sqrt(diffX * diffX + diffY * diffY);
		if (hypotenuse > 0 && hypotenuse < (idealSegmentLength + fromNode._width/4) * 4){
			var adjusted:Number = REPELK * (idealSegmentLength + fromNode._width/4) / hypotenuse * 2;
			spring.x -=  diffX / hypotenuse * adjusted;
			spring.y -=  diffY / hypotenuse * adjusted;
		}
		return spring;		
	}

}